[Android]相册列表加载过程性能优化


问题描述

在手机中有多个存有图片的文件夹,在recent界面清掉所有的应用,点击进入图库,切换到相册列表。发现要过几秒钟才能从空白页面开始显示文件夹。同时会引起另一问题,在有大量图片文件夹的情况下删除一个文件夹,这个被删除的文件夹还会在界面上显示一段时间,要过好几秒才会被覆盖掉。

分析与解决

优化整体分为几个部分,由于当时第一手截图和log都没有保存,所以整体以记录修改思路为主。

优化一

一般面对性能优化问题,首先要定位问题。有两个入手方向:

  1. 从代码逻辑角度分析,在代码流程关键处添加Log,通过复现操作观察哪些Log不符合预期。比如这个问题,就是每次刷新相册列表的时候, 刷新行为表现的很慢,可以很容易找到出问题的代码段。
  2. traceView 分析,适合性能异常不确定的问题。比如 Gallery 的启动速度,启动流程涉及很多流程,没 法从逻辑角度确定出问题的代码段。

针对当前问题,可以很明确是刷新相册列表的时候某些地方做了一些耗时的操作。所以就要先看看每次刷新列表的时候程序到底干了些什么。因此,需要打一个完整的 log 看看刷新的流程。
通过分析 log (很多 log 并没有保存,所以只做文字描述),一个感觉到异常的地方是:刷新相册的时候,打印出了数十个重复的相册名称,而且不同相册名重复的次数不一样。

所以查找打印相关 Log 的代码,发现是方法 ListAlbumSetDataAdapter.setLayoutInfo()中打印的。这也就意 味着这个方法进行了多次执行。

对照 traceView,这个方法的确是出现耗时问题的一个地方。

那么这个方法是做什么的呢? 这个方法就是对相册列表的每一项进行封面图像和文字更新的方法。因为涉及到了封面图像的生成显示,所以会比较耗时。而大量调用这个方法会导致这个耗时线性增加。像我手机里有 6 个相册,这个方法却执行了数十次。

那么是谁在调用这个方法呢?
有两个地方,ListAlbumSetDataAdapter.updateView()ListAlbumSetDataAdapter.getView()

先看 updateView(),这个方法是我们自己定义的:
通过在 updateView()处打印堆栈信息,一层层向上查看方法调用顺序,最终发现在 AlbumSetSlidingWindow.updateAllImageRequests()处调用的时候进行了二级循环,进而导致 updateView()循环多次调用。

for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 
    AlbumSetEntry entry = mData[i % mData.length]; 
    for (int j = 0; j < entry.coverLoader.size(); j++) {
         if (startLoadBitmap(entry.coverLoader.get(j))) ++mActiveRequestCount; 
    }
     ... 
}

外层循环是更新每个相册,内层循环是对当前相册内图片进行更新。但对相册来说只需要更新第一张封面图
片即可,所以对这里内层循环可以进行优化。否则遇到很多图片的相册,对每张图片循环一次的开销太大了:

for (int i = mActiveStart, n = mActiveEnd; i < n; ++i) { 
    AlbumSetEntry entry = mData[i % mData.length]; 
    if (entry.coverLoader.size > 0) { 
        startLoadBitmap(entry.coverLoader.get(0)); 
    }
     ... 
} 
++mActiveRequestCount;

优化期间和开发的同事讨论中得知有一个地方设置了一个 12 的范围,但不确定对这里执行有什么影响。我 在看 log 的时候,恰好发现了一个包含很多图片的相册打印了 12 次,而其他相册只打印了几次,所以猜想好 像相册图片超过 12 张就会只打印 12 次,于是新建一个相册尝试了一下,发现的确是这样。所以定位到设置 12 的代码,是在 MediaSet.getCoverMediaItem()处。看了一下代码,这里 12 是返回数组最大范围,相册有超过 12 张图片就返回 12 张。然而对返回的这 12 张之后的操作,结合代码并没有发现特别的操作,反而导致 setLayoutInfo()多执行了很多遍。所以这里也进行了优化。把 12 改成 1。对照AOSP源码,源码在这里也是用的 1。

经过后来一系列分析,发现改动这个 12 的效果和改动上面的循环是一样的。原因如下: 在 AlbumSetDataLoader.reloadTask.run() 中,会进行图片的加载,其中有一句:

info.cover = info.item.getCoverMediaItem();

就是获取我们把 list.size 从 12 改为 1 的地方。所以这里 info.cover 保存的 list.size = 1。

赋值之后会执行:

executeAndWait(new UpdateContent(info));

在 UpdateContent(info)中,会执行这段代码:

mCoverItem[pos] = info.cover;

这个 mCoverItem[]是干什么的?他的作用体现在 AlbumSetDataLoader.getCoverItem()中:

public List getCoverItem(int index) {
    try {
        assertIsActive(index); 
    } catch (Exception e) {
        e.printStackTrace();
        return null; 
    }
    return mCoverItem[index % mCoverItem.length]; 
}

这个 getCoverItem()是被 AlbumSetSlidingWindow.updateAlbumSetEntry()处调用:

List cover = mSource.getCoverItem(slotIndex); 
...
if(cover != null) { 
    resetEntry(entry, cover.size());
     for (int i = cover.size() -1 ; i >= 0 ; i--) { 
        ... 
        entry.coverLoader.set(....) 
        ... 
    } 
} 
...

所以这里就看明白了,entry.coverLoader.size 是和 getCoverMediaItem() 相关的。因此这两处改动一处即 可。

优化二

再看 getView()方法:
getView() 是 BaseAdapter 的方法,是每次刷新 List 都会调用的方法。
从 log 信息可以看到,在我有 5 个相册的时候,getView()执行了 150 次,也就是每个相册遍历了 30 遍。

关于 getView()方法的调用,一般是和布局文件的复杂度相关的,尤其是对没有指定 cell 高度的 listView, 系统需要模拟加载这些 view 去进行计算,然后计算得出一个高度,之后再把真正的 cell 加载出来。所以出 现 getView()重复调用的情况,就要关注一下布局情况。

找到 list 相关界面布局文件 albumset_fragment.xml


看到列表的 layout_heightwrap_content,这个地方就会有问题,因为是 wrap_content,列表计算高度 的时候就会先试着加载所有 cell,算出真正的高度,然后再去加载一遍 cell,算出界面需要显示的 cell 的数 量,确定之后再真正加载一遍(大概是这样)。如果相册很多,每次滑动也都会重复这些操作。 所以这里如果把 layout_height 改为 match_parent,那就为系统省去了很多计算。

改动后再看 log,发现 5 个相册的情况下,getView()只执行了 30 次,改之前可是执行了 150 次!

这时候再从 traceView 查找耗时操作,很明显的看到另一个耗时大户露出了尾巴: LocalMergeAlbum.getTotalMediaItemCount()方法。

优化三

回到之前的大尾巴:LocalMergeAlbum.getTotalMediaItemCount(),这个方法会执行到另一处方法 LocalAlbum.getMediaItemCount()

同样的问题:这个方法是干什么的?LocalAlbum.getMediaItemCount()执行了数据库的查询操作,其实是获取当前相册的图片信息。

谁在调用它? 在该方法里打印堆栈信息,发现好多地方在调用它,但是某个方法的一次执行,导致了连续重复 3-4 次调用数据库查询,这引起了我的注意。

public int getTotalMediaItemCount() {
    int count = 0;
    mVideoCount = 0;
     for (MediaSet set : mSources) { ---------------4 
        if(null == set) 
            continue; 
        count += set.getTotalMediaItemCount(); -------- 1 
        if(GalleryActivity.TV_LINK_DRM_HIDE_FLAG){ 
            count -= set.getDrmCount();
        } 

        if(set.getMediaSetType() == MEDIASET_TYPE_VIDEO){ -------------2 
            if (set instanceof LocalAlbum) { 
                mVideoCount = ((LocalAlbum)set).getTotalMediaItemVideoCount(); 
            } else if (set instanceof FavoriteAlbum) { 
                mVideoCount = ((FavoriteAlbum)set).getTotalMediaItemVideoCount(); } 
            } 
        Log.i(TAG, "-----combo++sub--count-"+set.getTotalMediaItemCount()); -----------3 
     } 
    return count;
}

如代码所示,LocalMergeAlbum.getTotalMediaItemCount()每执行 1 次,LocalAlbum.getMediaItemCount()至少执行 4 次!!

对代码做个注释:

  1. 不用解释了,直接就调用 1 次了
  2. 在 set.getMediaSetType()里也执行了 1 次
  3. 竟然在 log 里还执行了无关紧要的 1 次,之前从 traceView 发现一个 Log.i 耗时就源于这里。
  4. mSources 是 image 和 video 两个类型各循环一次.

接下来就优化代码。

  • 对于 3. 直接注释掉…
  • 对于4.把内部执行逻辑分为image和 Video,先判断再执行流程,减少不必要的流程,也就是在判断类型条件后的代码块中执行代码 1,而不是像当前这样每次都执行代码 1,然后再根据类型执行其他操作。
  • 对于 2. 因为重写该方法的地方比较多,所以只调整 LocalAlbum.getMediaSetType()内部逻辑。 这样,因为实际上 image 和 video 总是不同的文件夹,这个方法在优化后只会执行 1 次的 getTotalMediaItemCount()的方法。

    调整之后,每次执行这个函数就只做一次数据库查询了。

优化到这一步,初次加载相册和删除相册就已经很流畅了。但前面说了,调用 getMediaItemCount()的地方 很多,排查了一下,还有LocalAlbumSet.AlbumsLoader.run()中还有一处 int count = album.getMediaItemCount(); 其中 count 并没有使用,所以是一段无用代码,可以注释掉。

所以说,平时在加Log或者删除无用代码的时候,一定要注意把不再使用的耗时的方法都去掉。否则都是定时炸弹。

优化四

触发刷新相册有几个操作:

  1. 第一次打开相册的时候

  2. 删除相册的时候

  3. 添加相册的时候

    前三个发生在 ListAlbumSetFragment

  4. 添加相册后要从其他相册列表选择相片的时候,显示的 ListAlbumPickerFragment 也会刷新 List

前面的优化已经对第 1,2 点优化的很好了。然而对于 3,4 点还是有不流畅。

所以进行添加相册操作再看 log,通过堆栈信息,发现这个在操作下,getMediaItemCount()的执行基本全部来自于 AlbumDataLoader.reloadTask.run()

可以知道这是一个线程。但通过 log 信息,发现这个线程有大量重复执行的情况,而且一个非常明显的现象就是,log 信息很混乱。看到线程重复执行,而且运行不同步,线程ID也有大量重复,每次操作都会创建新的线程。

所以特意打印出线程信息以及开关线程的 log: resume 是创建线程, pause 是结束线程,当前 log 中 pause 从未执行

09-02 01:08:12.942 25843 25843 D UpdateThread: ======================>resume<====================

 09-02 01:08:12.959 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-359,5,main] 
  09-02 01:08:13.063 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-359,5,main] 
  09-02 01:08:13.140 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-359,5,main] 
  09-02 01:08:13.206 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-359,5,main] 
  09-02 01:08:20.625 25843 25843 D UpdateThread: ======================>resume<====================
 09-02 01:08:20.692 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:20.824 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:20.828 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-359,5,main] 
 09-02 01:08:20.893 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:20.899 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-359,5,main] 
 09-02 01:08:28.437 25843 25843 D UpdateThread: ======================>resume<====================
 09-02 01:08:28.697 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:28.707 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:28.796 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:28.886 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:28.890 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.048 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.064 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:29.065 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.107 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.127 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:29.149 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.184 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.185 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:29.224 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.228 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:29.258 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.283 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.326 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:29.355 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-380,5,main] 
 09-02 01:08:42.739 25843 25843 D UpdateThread: ======================>resume<====================
 09-02 01:08:46.630 25843 25843 D UpdateThread: ======================>resume<====================
 09-02 01:08:49.243 25843 25843 D UpdateThread: ======================>resume<====================
 09-02 01:08:52.546 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:08:52.571 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:52.608 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:08:52.630 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:08:52.659 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:08:52.699 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:09:00.288 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:09:00.294 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:09:00.329 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-368,5,main] 
 09-02 01:09:00.342 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:09:00.391 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:09:00.428 25843 25843 D UpdateThread: ------------------------------------------------------------------>Thread[Thread-392,5,main] 
 09-02 01:09:08.559 25843 25843 D UpdateThread: ======================>resume<====================
 09-02 01:09:13.460 25843 25843 D UpdateThread: ======================>resume<====================

可以看到,经过一些连续操作后线程越来越多。所以联想到应该有某处是关闭线程的。跟踪代码,发现在对应 Fragment 的生命周期 onPause 里应该是进行线程关闭的操作的,然而这段代码竟然被注释掉了。将注释打开,重新测试,发现这时候同一时间只有一条线程了。这样就避免了多个线程多次执行 getMediaItemCount()的方法了。 最后操作一下程序,现在的相册刷新过程就非常流畅了。和优化前完全是两种体验。

总结

面对这种有固定复现步骤的性能问题,

  • 首先要定位到操作执行的相关代码段,通过阅读分析,推断可能出现问题的地方。然后,在关键流程处添加Log,然后复现问题,拿到相关Log。
  • 抓TraceView或者Systrace,结合Log信息,对比CPU耗时的方法和执行次数很多的方法。
  • 通过前两个信息定位到问题函数。然后添加堆栈信息Log.printStackTrace(new Throwable()),找到调用顺序(也可以用traceView看,但log打印会更直观)进行分析调试

另外,也从中看到一些不规范的编码习惯造成的严重影响。比如,ListView布局引起的getView多次调用,这个就需要多学习,对相关机制多一些了解。比如Log里打印从数据库读取的数据,这个是需要注意的习惯,建议打Log的时候,直接打印变量,而不要直接调用方法。还有无用代码的删除,耗时间读取数据库,然后读出的数据没有任何作用,这个也是不应该的。


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录